//==============================================================================
// Copyright (c) 2008-2013 Niall Ryan. All Rights Reserved.
//==============================================================================

#include <gtest/gtest.h>

#include "hydra/atomic/Atomic.h"
#include "hydra/atomic/AtomicMarkablePtr.h"
#include "hydra/atomic/AtomicPair.h"
#include "hydra/thread/Thread.h"

using namespace Hydra;


template<typename T>
class AtomicTest : public ::testing::Test
{
protected:
    const static unsigned int AddSize = 1;

	static void WriterStatic(AtomicTest* test)	{ test->Writer(); }
	void Writer()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			if( i & 1 )		m_atomic = m_value1;
			else			m_atomic = m_value2;
		}
	}

	static void ReaderStatic(AtomicTest* test)	{ test->Reader(); }
	void Reader()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			T x = m_atomic;	//atomic read here, make sure only read once per iteration
			if( x == m_value1 )			++m_numValueA1;
			else if( x == m_value2 )	++m_numValueA2;
			else						++m_numInvalidA;
		}
	}

	static void CASIncrementerStatic(AtomicTest* test)	{ test->CASIncrementer(); }
	void CASIncrementer()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			T x, inc;
			do 
			{
				x = m_atomic;
				inc = (T)(intptr_t(x) + 1);	//little hacky to make it work with void*
			} while( m_atomic.compareAndSwap(inc, x) != x );
		}
	}

	static void GetAndSetterAStatic(AtomicTest* test)	{ test->GetAndSetterA(); }
	void GetAndSetterA()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			T x = m_atomic.getAndSet(i&1?m_value1:m_value2);
			if( x == m_value1 )			++m_numValueA1;
			else if( x == m_value2 )	++m_numValueA2;
			else						++m_numInvalidA;
		}
	}

	static void GetAndSetterBStatic(AtomicTest* test)	{ test->GetAndSetterB(); }
	void GetAndSetterB()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			T x = m_atomic.getAndSet(i&1?m_value1:m_value2);
			if( x == m_value1 )			++m_numValueB1;
			else if( x == m_value2 )	++m_numValueB2;
			else						++m_numInvalidB;
		}
	}

	static void GetAndAddIncrementerStatic(AtomicTest* test)	{ test->GetAndAddIncrementer(); }
	void GetAndAddIncrementer()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			m_atomic.getAndAdd((T)1);
		}
	}

	static void GetAndIncrementStatic(AtomicTest* test)	{ test->GetAndIncrement(); }
	void GetAndIncrement()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			m_atomic.getAndIncrement();
		}
	}

	static void GetAndDecrementStatic(AtomicTest* test)	{ test->GetAndDecrement(); }
	void GetAndDecrement()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			m_atomic.getAndDecrement();
		}
	}

	enum
	{
		NUM_ITERATIONS = 500000
	};

	Atomic<T> m_atomic;
	T m_value1;
	T m_value2;
	int m_numValueA1;
	int m_numValueA2;
	int m_numInvalidA;
	int m_numValueB1;
	int m_numValueB2;
	int m_numInvalidB;
};


typedef ::testing::Types<char, unsigned char, short, unsigned short, int, unsigned int, __int64, unsigned __int64, char*> AtomicTestTypes;
TYPED_TEST_CASE(AtomicTest, AtomicTestTypes);


TYPED_TEST(AtomicTest, Stuff)
{
	m_value1 = (TypeParam)(0x1234567890abcdef);
	m_value2 = (TypeParam)(0x13579bdf02468ace);

	//atomic read/write
	{
		m_atomic = m_value1;
		m_numValueA1 = m_numValueA2 = m_numInvalidA = 0;
		Thread t1(&WriterStatic, this);
		Thread t2(&ReaderStatic, this);
		t1.Start();	t2.Start();
		t1.Join();	t2.Join();
		EXPECT_TRUE(m_numInvalidA==0);
		EXPECT_TRUE(m_numValueA1+m_numValueA2==NUM_ITERATIONS);
	}
	//compareAndSwap
	{
		m_atomic = 0;
		Thread t1(&CASIncrementerStatic, this);
		Thread t2(&CASIncrementerStatic, this);
		t1.Start();	t2.Start();
		t1.Join();	t2.Join();
		TypeParam incResult = (TypeParam)(NUM_ITERATIONS*2);
		EXPECT_TRUE(m_atomic==incResult);
	}
	//getAndSet
	{
		m_atomic = m_value1;
		m_numValueA1 = m_numValueA2 = m_numInvalidA = 0;
		m_numValueB1 = m_numValueB2 = m_numInvalidB = 0;
		Thread t1(&GetAndSetterAStatic, this);
		Thread t2(&GetAndSetterBStatic, this);
		t1.Start();	t2.Start();
		t1.Join();	t2.Join();
		EXPECT_TRUE(m_numInvalidA==0);
		EXPECT_TRUE(m_numInvalidB==0);
		EXPECT_TRUE(m_numValueA1+m_numValueA2==NUM_ITERATIONS);
		EXPECT_TRUE(m_numValueB1+m_numValueB2==NUM_ITERATIONS);
	}
	//getAndAdd
	{
		m_atomic = 0;
		Thread t1(&GetAndAddIncrementerStatic, this);
		Thread t2(&GetAndAddIncrementerStatic, this);
		t1.Start();	t2.Start();
		t1.Join();	t2.Join();
		TypeParam incResult = (TypeParam)(NUM_ITERATIONS*2*AddSize);
		EXPECT_TRUE(m_atomic==incResult);
	}
	//getAndIncrement
	{
		m_atomic = 0;
		Thread t1(&GetAndIncrementStatic, this);
		Thread t2(&GetAndIncrementStatic, this);
		t1.Start();	t2.Start();
		t1.Join();	t2.Join();
		TypeParam incResult = (TypeParam)(NUM_ITERATIONS*2*AddSize);
		EXPECT_TRUE(m_atomic==incResult);
	}
	//getAndDecrement
	{
		m_atomic = 0;
		Thread t1(&GetAndDecrementStatic, this);
		Thread t2(&GetAndDecrementStatic, this);
		t1.Start();	t2.Start();
		t1.Join();	t2.Join();
		TypeParam decResult = (TypeParam)(-NUM_ITERATIONS*2*(int)AddSize);
		EXPECT_TRUE(m_atomic==decResult);
	}

	//test pre/post increment/decrement operators
	m_atomic = 0;
	EXPECT_TRUE(++m_atomic == (TypeParam)(1*AddSize));
	EXPECT_TRUE(m_atomic++ == (TypeParam)(1*AddSize));
	EXPECT_TRUE(m_atomic == (TypeParam)(2*AddSize));
	EXPECT_TRUE(m_atomic-- == (TypeParam)(2*AddSize));
	EXPECT_TRUE(m_atomic == (TypeParam)(1*AddSize));
	EXPECT_TRUE(--m_atomic ==(TypeParam)(0*AddSize));
}


//custom test required for bool since bool doesn't support arithmetic
class AtomicBoolTest : public ::testing::Test
{
protected:
	static void SetFalseStatic(AtomicBoolTest* test)	{ test->SetFalse(); }
	void SetFalse()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			bool x = m_atomic.getAndSet(false);
			if( x )
				++m_numFalseChanges;
		}
	}

	static void SetTrueStatic(AtomicBoolTest* test)	{ test->SetTrue(); }
	void SetTrue()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			bool x = m_atomic.getAndSet(true);
			if( !x )
				++m_numTrueChanges;
		}
	}

	enum
	{
		NUM_ITERATIONS = 500000
	};
	Atomic<bool> m_atomic;
	int m_numFalseChanges;
	int m_numTrueChanges;
};


TEST_F(AtomicBoolTest, Stuff)
{
	//impossible for read/write to be non-atomic for a bool, so no test for that

	//getAndSet
	m_atomic = true;
	EXPECT_TRUE(m_atomic.getAndSet(false)==true);
	EXPECT_TRUE(m_atomic==false);
	EXPECT_TRUE(m_atomic.getAndSet(true)==false);
	EXPECT_TRUE(m_atomic==true);

	m_atomic = false;
	m_numFalseChanges = m_numTrueChanges = 0;
	Thread t1(&SetFalseStatic, this);
	Thread t2(&SetTrueStatic, this);
	t1.Start();	t2.Start();
	t1.Join();	t2.Join();
	if( m_atomic )
		EXPECT_TRUE(m_numFalseChanges+1==m_numTrueChanges);
	else
		EXPECT_TRUE(m_numFalseChanges==m_numTrueChanges);
}


class AtomicMarkablePtrTest : public ::testing::Test
{
protected:
	static void WriterStatic(AtomicMarkablePtrTest* test)	{ test->Writer(); }
	void Writer()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			if( i & 1 )		m_atomic.set(m_value1, false);
			else			m_atomic.set(m_value2, true);
		}
	}

	static void ReaderStatic(AtomicMarkablePtrTest* test)	{ test->Reader(); }
	void Reader()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			bool mark;
			void* x = m_atomic.get(mark);	//atomic read here, make sure only read once per iteration
			if( (x == m_value1) && !mark )			++m_numValueA1;
			else if( (x == m_value2) && mark )		++m_numValueA2;
			else									++m_numInvalidA;
		}
	}

	static void CASIncrementerStatic(AtomicMarkablePtrTest* test)	{ test->CASIncrementer(); }
	void CASIncrementer()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			void* x;
			void* inc;
			bool mark;
			do 
			{
				x = m_atomic.get(mark);
				if( intptr_t(x)&2 )
				{
					if( !mark )	++m_numInvalidA;
				}
				else
				{
					if( mark )	++m_numInvalidA;
				}
				inc = (void*)(intptr_t(x) + 2);	//adding 2 since lowest bit cannot be used
			} while( !m_atomic.compareAndSwap(inc, !mark, x, mark) );
		}
	}

	enum
	{
		NUM_ITERATIONS = 500000
	};
	AtomicMarkablePtr<void> m_atomic;
	void* m_value1;
	void* m_value2;
	void* m_value3;
	int m_numValueA1;
	int m_numValueA2;
	int m_numInvalidA;
};


TEST_F(AtomicMarkablePtrTest, Stuff)
{
	m_value1 = (void*)(0x12345678);
	m_value2 = (void*)(0x5abcdef0);
	m_value3 = (void*)(0x680bde14);

	bool mark;
	void* x;
	m_atomic.set(m_value1, false);
	EXPECT_TRUE(m_atomic.getPtr() == m_value1);
	x = m_atomic.get(mark);
	EXPECT_TRUE(x == m_value1);
	EXPECT_TRUE(!mark);
	m_atomic.set(m_value2, true);
	EXPECT_TRUE(m_atomic.getPtr() == m_value2);
	x = m_atomic.get(mark);
	EXPECT_TRUE(x == m_value2);
	EXPECT_TRUE(mark);

	EXPECT_TRUE(!m_atomic.compareAndSwap(m_value1, false, m_value1, false));
	x = m_atomic.get(mark);
	EXPECT_TRUE(x == m_value2);
	EXPECT_TRUE(mark);
	EXPECT_TRUE(!m_atomic.compareAndSwap(m_value1, false, m_value2, false));
	x = m_atomic.get(mark);
	EXPECT_TRUE(x == m_value2);
	EXPECT_TRUE(mark);
	EXPECT_TRUE(m_atomic.compareAndSwap(m_value1, false, m_value2, true));
	x = m_atomic.get(mark);
	EXPECT_TRUE(x == m_value1);
	EXPECT_TRUE(!mark);

	m_atomic.compareAndSwap(m_value3, false, m_value2, true, x, mark);
	EXPECT_TRUE(x == m_value1);
	EXPECT_TRUE(!mark);
	x = m_atomic.get(mark);
	EXPECT_TRUE(x == m_value1);
	EXPECT_TRUE(!mark);
	m_atomic.compareAndSwap(m_value3, true, m_value1, false, x, mark);
	EXPECT_TRUE(x == m_value1);
	EXPECT_TRUE(!mark);
	x = m_atomic.get(mark);
	EXPECT_TRUE(x == m_value3);
	EXPECT_TRUE(mark);

	//atomic read/write
	{
		m_atomic.set(m_value1, false);
		m_numValueA1 = m_numValueA2 = m_numInvalidA = 0;
		Thread t1(&WriterStatic, this);
		Thread t2(&ReaderStatic, this);
		t1.Start();	t2.Start();
		t1.Join();	t2.Join();
		EXPECT_TRUE(m_numInvalidA==0);
		EXPECT_TRUE(m_numValueA1+m_numValueA2==NUM_ITERATIONS);
	}
	//compareAndSwap
	{
		m_atomic.set(0, false);
		m_numInvalidA = 0;
		Thread t1(&CASIncrementerStatic, this);
		Thread t2(&CASIncrementerStatic, this);
		t1.Start();	t2.Start();
		t1.Join();	t2.Join();
		EXPECT_TRUE(m_numInvalidA==0);
		EXPECT_TRUE(m_atomic.getPtr()==(void*)(NUM_ITERATIONS*4));
	}
}


template<typename T>
class AtomicStructTest : public ::testing::Test
{
protected:
	static void WriterStatic(AtomicStructTest* test)	{ test->Writer(); }
	void Writer()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			if( i & 1 )		m_atomic = m_value1;
			else			m_atomic = m_value2;
		}
	}

	static void ReaderStatic(AtomicStructTest* test)	{ test->Reader(); }
	void Reader()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			T x = m_atomic;	//atomic read here, make sure only read once per iteration
			if( x == m_value1 )			++m_numValueA1;
			else if( x == m_value2 )	++m_numValueA2;
			else						++m_numInvalidA;
		}
	}

	static void CASIncrementerStatic(AtomicStructTest* test)	{ test->CASIncrementer(); }
	void CASIncrementer()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			T x, inc;
			do 
			{
				x = m_atomic;
				inc = x;
				++inc;
			} while( !(m_atomic.compareAndSwap(inc, x) == x) );
		}
	}

	static void GetAndSetterAStatic(AtomicStructTest* test)	{ test->GetAndSetterA(); }
	void GetAndSetterA()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			T x = m_atomic.getAndSet(i&1?m_value1:m_value2);
			if( x == m_value1 )			++m_numValueA1;
			else if( x == m_value2 )	++m_numValueA2;
			else						++m_numInvalidA;
		}
	}

	static void GetAndSetterBStatic(AtomicStructTest* test)	{ test->GetAndSetterB(); }
	void GetAndSetterB()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			T x = m_atomic.getAndSet(i&1?m_value1:m_value2);
			if( x == m_value1 )			++m_numValueB1;
			else if( x == m_value2 )	++m_numValueB2;
			else						++m_numInvalidB;
		}
	}

	enum
	{
		NUM_ITERATIONS = 500000
	};

	Atomic<T> m_atomic;
	T m_value1;
	T m_value2;
	int m_numValueA1;
	int m_numValueA2;
	int m_numInvalidA;
	int m_numValueB1;
	int m_numValueB2;
	int m_numInvalidB;
};


struct ThreeBytes
{
	ThreeBytes() { }
	ThreeBytes(int x, int y) { m_x = x; m_y = y; }
	int operator++() { ++m_x; ++m_y; return m_x; }
	bool operator==(const ThreeBytes& tb) const { return (m_x==tb.m_x) && (m_y==tb.m_y); }
	short m_x;
	char m_y;
};

struct FiveBytes
{
	FiveBytes() { }
	FiveBytes(int x, int y) { m_x = x; m_y = y; }
	int operator++() { ++m_x; ++m_y; return m_x; }
	bool operator==(const FiveBytes& tb) const { return (m_x==tb.m_x) && (m_y==tb.m_y); }
	int m_x;
	char m_y;
};

struct SixBytes
{
	SixBytes() { }
	SixBytes(int x, int y) { m_x = x; m_y = y; }
	int operator++() { ++m_x; ++m_y; return m_x; }
	bool operator==(const SixBytes& tb) const { return (m_x==tb.m_x) && (m_y==tb.m_y); }
	int m_x;
	short m_y;
};

struct SevenBytes
{
	SevenBytes() { }
	SevenBytes(int x, int y) { m_x = x; m_y = y; }
	int operator++() { ++m_x; ++m_y; return m_x; }
	bool operator==(const SevenBytes& tb) const { return (m_x==tb.m_x) && (m_y==tb.m_y); }
	int m_x;
	short m_blah;
	char m_y;
};

typedef ::testing::Types<ThreeBytes, FiveBytes, SixBytes, SevenBytes> AtomicStructTestTypes;
TYPED_TEST_CASE(AtomicStructTest, AtomicStructTestTypes);

TYPED_TEST(AtomicStructTest, Stuff)
{
	m_value1 = TypeParam(0x12345678, 0x90abcdef);
	m_value2 = TypeParam(0x13579bdf, 0x02468ace);

	//atomic read/write
	{
		m_atomic = m_value1;
		m_numValueA1 = m_numValueA2 = m_numInvalidA = 0;
		Thread t1(&WriterStatic, this);
		Thread t2(&ReaderStatic, this);
		t1.Start();	t2.Start();
		t1.Join();	t2.Join();
		EXPECT_TRUE(m_numInvalidA==0);
		EXPECT_TRUE(m_numValueA1+m_numValueA2==NUM_ITERATIONS);
	}
	//compareAndSwap
	{
		m_atomic = TypeParam(0,0);
		Thread t1(&CASIncrementerStatic, this);
		Thread t2(&CASIncrementerStatic, this);
		t1.Start();	t2.Start();
		t1.Join();	t2.Join();
		TypeParam incResult = TypeParam(NUM_ITERATIONS*2,NUM_ITERATIONS*2);
		TypeParam result = m_atomic;
		EXPECT_TRUE(result==incResult);
	}
	//getAndSet
	{
		m_atomic = m_value1;
		m_numValueA1 = m_numValueA2 = m_numInvalidA = 0;
		m_numValueB1 = m_numValueB2 = m_numInvalidB = 0;
		Thread t1(&GetAndSetterAStatic, this);
		Thread t2(&GetAndSetterBStatic, this);
		t1.Start();	t2.Start();
		t1.Join();	t2.Join();
		EXPECT_TRUE(m_numInvalidA==0);
		EXPECT_TRUE(m_numInvalidB==0);
		EXPECT_TRUE(m_numValueA1+m_numValueA2==NUM_ITERATIONS);
		EXPECT_TRUE(m_numValueB1+m_numValueB2==NUM_ITERATIONS);
	}
}


class AtomicPairTest : public ::testing::Test
{
protected:
	static void WriterStatic(AtomicPairTest* test)	{ test->Writer(); }
	void Writer()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			if( i & 1 )		m_atomic.set(m_firstValue1, m_secondValue1);
			else			m_atomic.set(m_firstValue2, m_secondValue2);
		}
	}

	static void ReaderStatic(AtomicPairTest* test)	{ test->Reader(); }
	void Reader()
	{
		for( int i=0; i<NUM_ITERATIONS; ++i )
		{
			int first;
			char second;
			m_atomic.get(&first, &second);  //atomic read here, make sure only read once per iteration
			if( (first == m_firstValue1) && (second == m_secondValue1) )
				++m_numValue1;
			else if( (first == m_firstValue2) && (second == m_secondValue2) )
				++m_numValue2;
			else
				++m_numInvalid;
		}
	}

	enum
	{
		NUM_ITERATIONS = 500000
	};

	AtomicPair<int, char> m_atomic;
	int m_firstValue1;
	char m_secondValue1;
	int m_firstValue2;
	char m_secondValue2;
	int m_numValue1;
	int m_numValue2;
	int m_numInvalid;
};

TEST_F(AtomicPairTest, Stuff)
{
	m_firstValue1 = 0x12345678;
	m_secondValue1 = 42;
	m_firstValue2 = 0x90abcdef;
	m_secondValue2 = 27;

	//atomic read/write
	{
		m_atomic.set(m_firstValue1, m_secondValue1);
		m_numValue1 = m_numValue2 = m_numInvalid = 0;
		Thread t1(&WriterStatic, this);
		Thread t2(&ReaderStatic, this);
		t1.Start();	t2.Start();
		t1.Join();	t2.Join();
		EXPECT_TRUE(m_numInvalid==0);
		EXPECT_TRUE(m_numValue1+m_numValue2==NUM_ITERATIONS);
	}
}

//typedef AtomicTest<int*, sizeof(int)> AtomicIntPtrTest;


